/**
 * 
 */
package com.pxingg.equipment.powerswitch.Sevytec;

import java.util.Arrays;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.pxingg.datetime.TimeUtils;
import com.pxingg.datetime.Timer;
import com.pxingg.encrypt.crc.CRC;
import com.pxingg.exception.XException;
import com.pxingg.fep.serial.Serial;
import com.pxingg.utils.RangeUtils;

/**
 * @author pxingg
 * 
 */
public class SerialRelay12
{
    // ///////////////////////////////////////////////////////////
    // The constant variable member of the class
    // ////////////////////////////////////////////////////////////
    /**
     * The log of this class
     */
    static Log log = LogFactory.getLog(SerialRelay12.class);
    /**
     * The MIN NO of RELAY on V1.6 12 serial switch board
     */
    public final static int MIN_RELAY_NO = 0x01;
    /**
     * The MAX NO of RELAY on V1.6 12 serial switch board
     */
    public final static int MAX_RELAY_NO = 0x0C;
    /**
     * The relay switch is ON, or the input data is ON
     */
    public static final int SWITCH_ON = 0x01;
    /**
     * The relay switch is OFF, or the input data is OFF
     */
    public static final int SWITCH_OFF = 0x00;
    /**
     * The Total number of Relay Switch
     */
    public static final int TOTAL_RELAY = 12;
    /**
     * The Total number of Input Switch
     */
    public static final int TOTAL_INPUT = 12;
    /**
     * The Total number of ADCInput Switch
     */
    public static final int TOTAL_ADC_INPUT = 2;
    /**
     * The MIN equipment no in the SEVYTEC switch RS485 net work
     */
    public static final int MIN_EQUIPMENT_NO = 0x01;
    /**
     * The MAX equipment no in the SEVYTEC switch RS485 net work
     */
    public static final int MAX_EQUIPMENT_NO = 0xFE;
    /**
     * The MAX wait time from send request and receive response message
     */
    public static final long MAX_TIME_WAIT_INTERVAL = 500;
    /**
     * The default sleep interval for process the message
     */
    public static final long DEFAULT_SLEEP_INTERVAL = 5;
    /**
     * The default interval for switch change status
     */
    public static final long SWITCH_CHANGE_INTERVAL = 20;
    /**
     * The Message header of the production of SEVYTEC relay switch production
     */
    protected static final byte MESSAGE_HEADER[] = {
        (byte) 0xAA, (byte) 0xBB
    };
    /**
     * The max message length of the serial switch
     */
    protected static final int MAX_MESSAGE_LENGTH = 8;

    /**
     * The message length of the query version request message
     */
    protected static final int VERSION_REQUEST_MESSAGE_LENGTH = 5;
    /**
     * The message length of the query version response message
     */
    protected static final int VERSION_RESPONSE_MESSAGE_LENGTH = 4;
    /**
     * The message length of the switch on or off request message, <br>
     * and these message do not have response message
     */
    protected static final int SWITCH_ON_OFF_REQUEST_MESSAGE_LENGTH = 5;
    /**
     * The message length of the switch status request message
     */
    protected static final int STATUS_REQUEST_MESSAGE_LENGTH = 5;
    /**
     * The max message length of switch status response message
     */
    protected static final int STATUS_RESPONSE_MESSAGE_LENGTH = 8;
    /**
     * The message length of the ADC request message
     */
    protected static final int ADC_REQUEST_MESSAGE_LENGTH = 5;
    /**
     * The max message length of ADC response message
     */
    protected static final int ADC_RESPONSE_MESSAGE_LENGTH = 8;

    // ///////////////////////////////////////////////////////////
    // The member of the switch
    // ////////////////////////////////////////////////////////////
    /**
     * The equipment no in the Switch RS485 network
     */
    protected int equipmentNo = 0;
    /**
     * The Serial bus the device connect to
     */
    protected Serial rs485Bus = null;
    /**
     * The status of the switch
     */
    protected int[] switchStatus = new int[SerialRelay12.TOTAL_RELAY];
    /**
     * The status of input
     */
    protected int[] inputStatus = new int[SerialRelay12.TOTAL_INPUT];
    /**
     * The status of input
     */
    protected int[] ADCInputStatus = new int[SerialRelay12.TOTAL_ADC_INPUT];

    /**
     * Indicate if the Switch is connected to the Bus
     */
    protected boolean connected = false;

    /**
     * The buffer used to send and receive message
     */
    protected byte[] buffer = new byte[SerialRelay12.MAX_MESSAGE_LENGTH];
    /**
     * Timer is used to calculate the interval of send and receive the message
     */
    Timer timer = new Timer();
    /**
     * Used to record lasted the error information in the operation
     */
    String errorInfo = "";

    // ///////////////////////////////////////////////////////////
    // The function of the switch
    // ////////////////////////////////////////////////////////////
    /**
     * Construction of the SerialRelay12
     * 
     * @param equipmentNo
     *            The equipment no of the switch
     * @param rs485Bus
     *            The RS485 bus the switch connected
     */
    public SerialRelay12(final int equipmentNo, final Serial rs485Bus)
    {
        this.equipmentNo = equipmentNo;
        this.rs485Bus = rs485Bus;
        connected = false;

        // fill the message header
        buffer[0] = SerialRelay12.MESSAGE_HEADER[0];
        buffer[1] = SerialRelay12.MESSAGE_HEADER[1];
        buffer[2] = (byte) this.equipmentNo;

        for (int i = 0; i < SerialRelay12.TOTAL_RELAY; i++)
        {
            // initial all switch off
            switchStatus[i] = SerialRelay12.SWITCH_OFF;
        }

        for (int i = 0; i < SerialRelay12.TOTAL_INPUT; i++)
        {
            // initial all switch off
            inputStatus[i] = SerialRelay12.SWITCH_OFF;
        }

        for (int i = 0; i < SerialRelay12.TOTAL_ADC_INPUT; i++)
        {
            // initial all ADC input to 0
            inputStatus[i] = 0;
        }
    }

    /**
     * Connect the Switch to the Rs485 Bus
     * 
     * @throws XException
     *             if can not connect to the RS485 Bus
     */
    public void connect() throws XException
    {
        if (connected)
        {
            return;
        }
        if (rs485Bus == null)
        {
            errorInfo = "The serial bus is not set in the Serial switch";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        if (RangeUtils.isBeyondLPRP(SerialRelay12.MIN_EQUIPMENT_NO, SerialRelay12.MAX_EQUIPMENT_NO, equipmentNo))
        {
            errorInfo = "equipmentNo " + equipmentNo + " is invalid";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        if (!rs485Bus.isConnected())
        {
            errorInfo = "the RS485 Bus is not connected";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        // now send a version query message to the switch to make sure the
        // switch is connected

        final String version = queryVersion();
        if ("V1.3".equals(version))
        {
            connected = true;
        }
        else
        {
            // can not get the version of the board, do not think the switch
            // connected to the bus
            errorInfo = "can not get the verion of SerialRelay12 of equipmentNo " + equipmentNo;
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        // And now, Just On all the switch that need on
        for (int i = 0; i < SerialRelay12.TOTAL_RELAY; i++)
        {
            if (switchStatus[i] == SerialRelay12.SWITCH_ON)
            {
                switchOn(i);
            }
        }
    }

    /**
     * Get the ADC status array
     * 
     * @return the ADC status array
     */
    public int[] getADCInputStatus()
    {
        return ADCInputStatus;
    }

    /**
     * Get the input status array
     * 
     * @return the input status array
     */
    public int[] getInputStatus()
    {
        return inputStatus;
    }

    /**
     * Get the switch status array
     * 
     * @return the switch status array
     */
    public int[] getSwitchStatus()
    {
        return switchStatus;
    }

    /**
     * Get the status of all the ADC
     * 
     * @throws XException
     */
    public void queryADC() throws XException
    {
        if (!rs485Bus.isConnected())
        {
            errorInfo = "the RS485 Bus is not connected";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        buffer[3] = (byte) 0x1D;
        buffer[4] = (byte) 0x01;

        final byte[] response = sendAndReceive(SerialRelay12.ADC_REQUEST_MESSAGE_LENGTH, SerialRelay12.ADC_RESPONSE_MESSAGE_LENGTH);

        if (response == null)
        {
            errorInfo = "send the status request message, but can not get the response message";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        if (response[0] != SerialRelay12.MESSAGE_HEADER[0] && response[1] != SerialRelay12.MESSAGE_HEADER[1])
        {
            errorInfo = "the header of response data(" + Arrays.toString(response) + ") is invalid";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        final int equipNo = (response[2] & 0xFF);
        if (equipNo != equipmentNo)
        {
            errorInfo = "the reponse equipment no " + equipNo + " is not expected equipment no " + equipmentNo;
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        if (CRC.sumCRC8(response, SerialRelay12.STATUS_RESPONSE_MESSAGE_LENGTH - 1) != response[response.length - 1])
        {
            errorInfo = "the data(" + Arrays.toString(response) + ") is invalid for the CRC is not match";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        // The first ADC value of the board
        ADCInputStatus[0] = (response[3] & 0xFF) + (response[4] & 0xFF) * 256;
        // The second ADC value of the board
        ADCInputStatus[1] = (response[5] & 0xFF) + (response[6] & 0xFF) * 256;
        TimeUtils.mySleep(SerialRelay12.SWITCH_CHANGE_INTERVAL);
    }

    /**
     * Get the status of all the switch and input
     * 
     * @throws XException
     */
    public void queryStatus() throws XException
    {
        if (!rs485Bus.isConnected())
        {
            errorInfo = "the RS485 Bus is not connected";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        buffer[3] = (byte) 0x0D;
        buffer[4] = (byte) 0x01;

        final byte[] response = sendAndReceive(SerialRelay12.STATUS_REQUEST_MESSAGE_LENGTH, SerialRelay12.STATUS_RESPONSE_MESSAGE_LENGTH);

        if (response == null)
        {
            errorInfo = "send the status request message, but can not get the response message";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        if (response[0] != SerialRelay12.MESSAGE_HEADER[0] || response[1] != SerialRelay12.MESSAGE_HEADER[1])
        {
            errorInfo = "the header of response data(" + Arrays.toString(response) + ") is invalid";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        final int equipNo = (response[2] & 0xFF);
        if (equipNo != equipmentNo)
        {
            errorInfo = "the reponse equipment no " + equipNo + " is not expected equipment no " + equipmentNo;
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        if (CRC.sumCRC8(response, SerialRelay12.STATUS_RESPONSE_MESSAGE_LENGTH - 1) != response[response.length - 1])
        {
            errorInfo = "the data(" + Arrays.toString(response) + ") is invalid for the CRC is not match";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        // analysis the status of the switch
        int status = ((response[4] & 0xFF) << 8) + (response[3] & 0xFF);
        for (int i = 0; i < SerialRelay12.TOTAL_RELAY; i++)
        {
            if ((status & (0x01 << i)) > 0)
            {
                switchStatus[i] = SerialRelay12.SWITCH_ON;
            }
            else
            {
                switchStatus[i] = SerialRelay12.SWITCH_OFF;
            }
        }

        // analysis the status of the input
        status = ((response[6] & 0xFF) << 8) + (response[5] & 0xFF);
        for (int i = 0; i < SerialRelay12.TOTAL_INPUT; i++)
        {
            if ((status & (0x01 << i)) > 0)
            {
                inputStatus[i] = SerialRelay12.SWITCH_ON;
            }
            else
            {
                inputStatus[i] = SerialRelay12.SWITCH_OFF;
            }
        }
        TimeUtils.mySleep(SerialRelay12.SWITCH_CHANGE_INTERVAL);
    }

    /**
     * Get the version of the serial relay switch board
     * 
     * @return the version of the serial relay switch board
     * @throws XException
     */
    public String queryVersion() throws XException
    {
        if (!rs485Bus.isConnected())
        {
            errorInfo = "the RS485 Bus is not connected";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        buffer[3] = (byte) 0x0F;
        buffer[4] = (byte) 0x01;

        final byte[] response = sendAndReceive(SerialRelay12.VERSION_REQUEST_MESSAGE_LENGTH, SerialRelay12.VERSION_RESPONSE_MESSAGE_LENGTH);

        if (response == null)
        {
            errorInfo = "send the version request message, but can not get the response message";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        if (response[0] != SerialRelay12.MESSAGE_HEADER[0] && response[1] != SerialRelay12.MESSAGE_HEADER[1])
        {
            errorInfo = "the header of response data(" + Arrays.toString(response) + ") is invalid";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        final int equipNo = (response[2] & 0xFF);
        if (equipNo != equipmentNo)
        {
            errorInfo = "the reponse equipment no " + equipNo + " is not expected equipment no " + equipmentNo;
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }
        final String version = "V" + ((response[3] & 0xF0) >> 4) + "." + (response[3] & 0x0F);

        return version;
    }

    /**
     * Send the message to the switch and try to get the response in a defined interval
     * 
     * @param requestLength
     *            the length in the buffer need to send
     * @param responseLength
     *            the except length of the response message
     * @return
     */
    protected byte[] sendAndReceive(final int requestLength, final int responseLength)
    {
        byte[] response = null;

        synchronized (rs485Bus)
        {
            // clear the data in the send and receive buffer first
            rs485Bus.getSendBuffer().clear();
            rs485Bus.getReceiveBuffer().clear();
            // put the data in the buffer and begin the timer
            rs485Bus.getSendBuffer().append(buffer, requestLength);
            timer.start();

            while (true)
            {
                response = rs485Bus.getReceiveBuffer().serve(responseLength);
                if (response != null)
                {
                    // have get the response
                    break;
                }
                if (timer.getInterval() > SerialRelay12.MAX_TIME_WAIT_INTERVAL)
                {
                    // have wait enough time, but the response is not response
                    // clear the message that not send, and will not send it
                    // again
                    connected = false;
                    rs485Bus.getSendBuffer().clear();
                    errorInfo = "have wait " + SerialRelay12.MAX_TIME_WAIT_INTERVAL + " macro seconds, but the response of the message is not get";
                    SerialRelay12.log.error(errorInfo);
                    break;
                }

                TimeUtils.mySleep(SerialRelay12.DEFAULT_SLEEP_INTERVAL);
            }
        }

        return response;
    }

    /**
     * Send the message to the switch and without receive message
     * 
     * @param requestLength
     *            the length in the buffer need to send
     * @param interval
     *            the wait interval for the message to send
     * @throws XException
     */
    protected void sendWithoutReceive(final int requestLength, final long interval) throws XException
    {
        synchronized (rs485Bus)
        {
            // clear the data in the send and receive buffer first
            rs485Bus.getSendBuffer().clear();
            rs485Bus.getReceiveBuffer().clear();
            // put the data in the buffer and begin the timer
            rs485Bus.getSendBuffer().append(buffer, requestLength);

            timer.start();
            while (rs485Bus.getSendBuffer().size() > 0)
            {
                if (timer.getInterval() > interval)
                {
                    // clear the message that not send, and will not send it
                    // again
                    connected = false;
                    rs485Bus.getSendBuffer().clear();
                    errorInfo = "have wait " + interval + " macro seconds, but the message is not complete send";
                    SerialRelay12.log.error(errorInfo);
                    throw new XException(errorInfo);
                }
                TimeUtils.mySleep(SerialRelay12.DEFAULT_SLEEP_INTERVAL);
            }
        }
    }

    /**
     * Make all the switch No(1 - 12) OFF
     * 
     * @throws XException
     */
    public void switchAllOff() throws XException
    {
        if (!rs485Bus.isConnected())
        {
            errorInfo = "the RS485 Bus is not connected";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        buffer[3] = (byte) 0x1C;
        buffer[4] = (byte) 0x01;

        sendWithoutReceive(SerialRelay12.SWITCH_ON_OFF_REQUEST_MESSAGE_LENGTH, SerialRelay12.MAX_TIME_WAIT_INTERVAL);

        // sleep SWITCH_CHANGE_INTERVAL macro sends to wait the message is send
        // and the switch status is changed
        TimeUtils.mySleep(SerialRelay12.SWITCH_CHANGE_INTERVAL);

        // Begin to to the check of the command
        queryStatus();
        boolean isAllOff = true;
        for (int i = 0; i < SerialRelay12.TOTAL_RELAY; i++)
        {
            if (switchStatus[i] != SerialRelay12.SWITCH_OFF)
            {
                SerialRelay12.log.error("on the equiment no " + equipmentNo + " switch no " + (i + 1) + " is not off by switchAllOff command");
                isAllOff = false;
            }
        }

        if (!isAllOff)
        {
            throw new XException("on the equiment no " + equipmentNo + " not all switch is on by switchAllOn command");
        }
    }

    /**
     * Make all the switch No(1 - 12) ON
     * 
     * @throws XException
     */
    public void switchAllOn() throws XException
    {
        if (!rs485Bus.isConnected())
        {
            errorInfo = "the RS485 Bus is not connected";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        buffer[3] = (byte) 0x1A;
        buffer[4] = (byte) SerialRelay12.SWITCH_ON;

        sendWithoutReceive(SerialRelay12.SWITCH_ON_OFF_REQUEST_MESSAGE_LENGTH, SerialRelay12.MAX_TIME_WAIT_INTERVAL);

        // sleep SWITCH_CHANGE_INTERVAL macro sends to wait the message is send
        // and the switch status is changed
        TimeUtils.mySleep(SerialRelay12.SWITCH_CHANGE_INTERVAL);

        // Begin to to the check of the command
        queryStatus();
        boolean isAllOn = true;
        for (int i = 0; i < SerialRelay12.TOTAL_RELAY; i++)
        {
            if (switchStatus[i] != SerialRelay12.SWITCH_ON)
            {
                SerialRelay12.log.error("on the equiment no " + equipmentNo + " switch no " + (i + 1) + " is not on by switchAllOn command");
                isAllOn = false;
            }
        }

        if (!isAllOn)
        {
            throw new XException("on the equiment no " + equipmentNo + " not all switch is on by switchAllOn command");
        }
    }

    /**
     * Switch off the excepted switch ON
     * 
     * @param switchNo
     *            The No(1 - 12) of switch that want it off
     * @throws XException
     */
    public void switchOff(final int switchNo) throws XException
    {
        if (!rs485Bus.isConnected())
        {
            errorInfo = "the RS485 Bus is not connected";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        if (RangeUtils.isBeyondLPRP(1, SerialRelay12.TOTAL_RELAY, switchNo))
        {
            throw new XException("the switch no " + switchNo + " is invalid");
        }

        buffer[3] = (byte) switchNo;
        buffer[4] = (byte) SerialRelay12.SWITCH_OFF;

        sendWithoutReceive(SerialRelay12.SWITCH_ON_OFF_REQUEST_MESSAGE_LENGTH, SerialRelay12.MAX_TIME_WAIT_INTERVAL);

        // sleep SWITCH_CHANGE_INTERVAL macro sends to wait the message is send
        // and the switch status is changed
        TimeUtils.mySleep(SerialRelay12.SWITCH_CHANGE_INTERVAL);

        // Begin to to the check of the command
        queryStatus();
        if (switchStatus[switchNo - 1] != SerialRelay12.SWITCH_OFF)
        {
            errorInfo = "on the equiment no " + equipmentNo + " switch no " + switchNo + " is not off by switchOff command";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }
    }

    /**
     * Switch on the excepted switch ON
     * 
     * @param switchNo
     *            The No(1 - 12) of switch that want it on
     * @throws XException
     */
    public void switchOn(final int switchNo) throws XException
    {
        if (!rs485Bus.isConnected())
        {
            errorInfo = "the RS485 Bus is not connected";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }

        if (RangeUtils.isBeyondLPRP(1, SerialRelay12.TOTAL_RELAY, switchNo))
        {
            throw new XException("the switch no " + switchNo + " is invalid");
        }

        buffer[3] = (byte) switchNo;
        buffer[4] = (byte) SerialRelay12.SWITCH_ON;

        sendWithoutReceive(SerialRelay12.SWITCH_ON_OFF_REQUEST_MESSAGE_LENGTH, SerialRelay12.MAX_TIME_WAIT_INTERVAL);

        // sleep SWITCH_CHANGE_INTERVAL macro sends to wait the message is send
        // and the switch status is changed
        TimeUtils.mySleep(SerialRelay12.SWITCH_CHANGE_INTERVAL);

        // Begin to to the check of the command
        queryStatus();
        if (switchStatus[switchNo - 1] != SerialRelay12.SWITCH_ON)
        {
            errorInfo = "on the equiment no " + equipmentNo + " switch no " + switchNo + " is not on by switchOn command";
            SerialRelay12.log.error(errorInfo);
            throw new XException(errorInfo);
        }
    }

    /**
     * (non-Javadoc)
     * 
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString()
    {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
    }
}
